﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Threading;
using System.Web;
using Lucene.Net.Analysis;
using Lucene.Net.Analysis.Standard;
using Lucene.Net.Highlight;
using Lucene.Net.QueryParsers;
using Lucene.Net.Search;
using umbraco.cms.businesslogic.web;
using umbraco.presentation.webservices;

namespace UmbSearch2.Search
{
	public class Searcher
	{
		private static string[] searchFields;
		private static Query query;
		private static int numberOfTextFragments = 5;
		private static int numberOfCharactersInTextFragment = 100;
        
		public static List<SearchResultItem> Search(string searchKeywords, string[] userDefinedSearchFields, int searchStartNodeId, int misspelledWordsNodeId, ArrayList documentTypes)
		{
			// replace the misspelled words in the query
			if (misspelledWordsNodeId != 0)	{
				
				searchKeywords = ReplaceMisspelledSearchKeywords(misspelledWordsNodeId, searchKeywords);
			}

			// optimize searchKeywords
			searchKeywords = StripInput(searchKeywords);
			
			searchFields = new string[userDefinedSearchFields.Length + 2];
			for (int i = 0; i < userDefinedSearchFields.Length; i++)
			{
				searchFields[i] = userDefinedSearchFields[i].Trim();
			}

			searchFields[userDefinedSearchFields.Length] = FieldName.FileContent.ToString();
			searchFields[userDefinedSearchFields.Length + 1] = FieldName.Title.ToString();

			IndexSearcher searcher = null;
			List<SearchResultItem> results = null;

			try
			{
				if (!Indexer.IsReindexing())
				{
					searcher = new IndexSearcher(Indexer.IndexDirectory);

					BooleanClause.Occur[] bc = new BooleanClause.Occur[searchFields.Length];
					for (int i = 0; i < bc.Length; i++)
					{
						bc[i] = BooleanClause.Occur.SHOULD;
					}

					query = MultiFieldQueryParser.Parse(searchKeywords, searchFields, bc, new StandardAnalyzer());
					results = ConvertHitsToSearchResultItems(searcher.Search(query), searchStartNodeId, documentTypes);
				}
			}
			catch(Exception ex)
			{
				// index is broken --> create a new index
				//if (!Indexer.IsReindexing())
				//{
				//    Indexer.ReIndex();
				//}
			}
			finally
			{
				if (searcher != null) 
					searcher.Close();
			}

			return results;
		}

		// strips detached wildcards like * and ?. The wildcards only work when attached to an character.
		// also removes extra spaces and adds 'AND' between the query words
		public static string StripInput(string input)
		{
			input = input.ToLower();

			input = input.Replace("**", "*");
			input = input.Replace("*?", "*");
			input = input.Replace("?*", "*");

			do
			{
				input = input.Replace("**", "*");
			}
			while (input.Contains("**"));

			// remove excessive spaces
			do
			{
				input = input.Replace("  ", " ");
			}
			while (input.Contains("  "));

			input = input.Replace(" and ", " AND ");
			input = input.Replace(" or ", " OR ");

			string[] inputSplitted = input.Split(' ');

			StringBuilder tempQueryBuilder = new StringBuilder();

			for (int i = 0; i < inputSplitted.Length; i++)
			{
				if (inputSplitted[i] != "*" && inputSplitted[i] != "?")
				{
					tempQueryBuilder.Append(inputSplitted[i]);
					// remove spaces after '+' and '-'
					if (inputSplitted[i] != "+" && inputSplitted[i] != "-")
					{
						tempQueryBuilder.Append(' ');
					}
				}
			}

			inputSplitted = tempQueryBuilder.ToString().Trim().Split(' ');

			StringBuilder queryBuilder = new StringBuilder();

			for (int i = 0; i < inputSplitted.Length; i++)
			{
				queryBuilder.Append(inputSplitted[i]);

				try // try catch necessary for index out of range exception
				{
					if (inputSplitted[i] != "AND" && inputSplitted[i + 1] != "AND" && inputSplitted[i] != "OR" && inputSplitted[i + 1] != "OR")
					{
						queryBuilder.Append(" AND ");
					}
					else
					{
						queryBuilder.Append(' ');
					}
				}
				catch (Exception) { }
			}

			return queryBuilder.ToString().Trim();
		}
		
		private static string ReplaceMisspelledSearchKeywords(int misspelledWordsNodeId, string searchKeywords)
		{
			Document misspelledWordsDoc = new Document(misspelledWordsNodeId);
			string misspelledWords = misspelledWordsDoc.getProperty("misspelledWords").Value.ToString();
			string[] stringSeparator = new string[] { Environment.NewLine };
			string[] misspelledWordsArray = misspelledWords.Split(stringSeparator, StringSplitOptions.RemoveEmptyEntries);

			Dictionary<string,string> misspelledWordsCollection = new Dictionary<string, string>();

			for (int i = 0; i < misspelledWordsArray.Length; i++)
			{
				string[] item = misspelledWordsArray[i].Split(':');
				string[] keys = item[1].Split(',');
				string value = item[0];
				for (int j = 0; j < keys.Length; j++)
				{
					misspelledWordsCollection.Add(keys[j].Trim(),value);
				}
			}

			string[] searchKeywordsArray = searchKeywords.Split(' ');

			StringBuilder correctedSearchKeywords = new StringBuilder();

			for (int i = 0; i < searchKeywordsArray.Length; i++)
			{
				if(misspelledWordsCollection.ContainsKey(searchKeywordsArray[i]))
				{
					correctedSearchKeywords.Append(misspelledWordsCollection[searchKeywordsArray[i]] + " ");
				}
				else
				{
					correctedSearchKeywords.Append(searchKeywordsArray[i] + " ");
				}
			}
			return correctedSearchKeywords.ToString();
		}

		private static List<SearchResultItem> ConvertHitsToSearchResultItems(Hits hits, int searchStartNodeId, ArrayList documentTypes)
		{
			List<SearchResultItem> results = new List<SearchResultItem>();

			for (int i = 0; i < hits.Length(); i++)
			{
				SearchResultItem resultItem = new SearchResultItem();
				resultItem.Id = int.Parse(hits.Doc(i).Get(FieldName.Id.ToString()));
				resultItem.DocumentType = hits.Doc(i).Get(FieldName.DocumentType.ToString());

				if (resultItem.ObjectType == Document._objectType)
				{
					// nodes that are not children of the searchStartNode shouldn't be added to the result set
					if (!NodeIsChildOfSearchStartNodeId(searchStartNodeId, resultItem.Id))
					{
						continue;
					}

					// only nodes from certain DocumentTypes have to be added to the collection
					if (!documentTypes.Contains(resultItem.DocumentType))
					{
						continue;
					}

					Document doc = new Document(resultItem.Id);

					// when a page is protected check if user has rights to see the page
					if(umbraco.library.IsProtected(resultItem.Id, doc.Path))
					{
						if(!umbraco.library.HasAccess(resultItem.Id,doc.Path))
							continue;
					}

					// don't show document when there is no template
					if (doc.Template == 0)
					{
						continue;
					}
				}

				resultItem.Title = hits.Doc(i).Get(FieldName.Title.ToString());
				resultItem.ObjectType = new Guid(hits.Doc(i).Get(FieldName.ObjectType.ToString()));
				resultItem.CreateDate = hits.Doc(i).Get(FieldName.CreateDate.ToString());
				if (!string.IsNullOrEmpty(hits.Doc(i).Get(FieldName.Extension.ToString())))
					resultItem.Extension = hits.Doc(i).Get(FieldName.Extension.ToString());
				if (!string.IsNullOrEmpty(hits.Doc(i).Get(FieldName.FilePath.ToString())))
					resultItem.FilePath = hits.Doc(i).Get(FieldName.FilePath.ToString());
				if (!string.IsNullOrEmpty(hits.Doc(i).Get(FieldName.FileContent.ToString())))
					resultItem.FileContent = hits.Doc(i).Get(FieldName.FileContent.ToString());
				resultItem.Match = CreateMatch(hits, i);
				resultItem.Summary = CreateSummary(hits.Doc(i));

				results.Add(resultItem);
			}
			return results;
		}

		private static bool NodeIsChildOfSearchStartNodeId(int parentId, int childId)
		{
			Document d = new Document(childId);
			string[] parentNodeIds = d.Path.Split(',');
			foreach (var id in parentNodeIds)
			{
				if (int.Parse(id) == parentId)
					return true;
			}
			return false;
		}
        
		private static string CreateMatch(Hits hits, int i)
		{
			NumberFormatInfo nfi = new NumberFormatInfo();
			nfi.PercentDecimalDigits = 0;
			return hits.Score(i).ToString("P", nfi);
		}

		private static string CreateSummary(Lucene.Net.Documents.Document doc)
		{
			StringBuilder contentBuilder = new StringBuilder();

			foreach (string s in searchFields)
			{
				// It is not necessary to show the title twice
				if (s != FieldName.Title.ToString() && !string.IsNullOrEmpty(doc.Get(s)))
				{
					contentBuilder.Append(umbraco.library.StripHtml(doc.Get(s)));
				}
			}

			string content = contentBuilder.ToString();

			SimpleHTMLFormatter formatter = new SimpleHTMLFormatter("<span class=\"highlight\">", "</span>");
			// "field" is a dummy variable. Lucene will not do anything with it
			TokenStream tokenStream = new StandardAnalyzer().TokenStream("field", new System.IO.StringReader(content));

			// necessary for using * and ? in search queries
			query = query.Rewrite(Indexer.GetIndexReader());

			Highlighter highlighter = new Highlighter(formatter, new QueryScorer(query));
			Fragmenter fragmenter = new SimpleFragmenter(numberOfCharactersInTextFragment);
			highlighter.SetTextFragmenter(fragmenter);
            
			string[] fragments = highlighter.GetBestFragments(tokenStream, content, numberOfTextFragments);
			StringBuilder summaryBuilder = new StringBuilder();
			foreach (string fragment in fragments)
			{
				string s = fragment.Replace("\v", "");
				
				// remove hard returns and replace with a single space
				s = s.Replace("\r\n", " ");
				while (s.Contains("  "))
				{
					s = s.Replace("  ", " ");
				}

				if (s.StartsWith("."))
				{
					string t = s.Remove(0, 1);
					summaryBuilder.Append(t.Trim());
				}
				else
				{
					summaryBuilder.Append(s.Trim());
				}

				if (!s.EndsWith("."))
				{
					summaryBuilder.Append("...");
				}
			}

			return summaryBuilder.ToString();
		}
	}
}